Skip to content

Rust Web 错误处理与中间件设计

1. 这是什么

当你已经会写 Axum 或 Actix Web 的基础路由后,下一步很快就会遇到两个工程问题:

  • 错误怎么统一处理
  • 横切逻辑应该放在哪里

这篇讲的就是这两件事:

  • 错误处理:请求失败后,应该怎样稳定、清晰地对外表达
  • 中间件设计:日志、鉴权、请求追踪、限流、上下文注入这类横切逻辑,应该怎样组织

一句话理解:

  • handler 负责处理当前请求的业务入口
  • 错误处理负责定义失败语义
  • 中间件负责承载跨多个路由共享的横切规则

2. 为什么这两个主题总会一起出现

因为在真实 Web 服务里,它们都属于“边界层治理”。

当请求进入系统时,你要决定:

  • 路由怎么分发
  • 参数怎么提取
  • 业务怎么调用
  • 错误怎么映射
  • 日志和 tracing 怎么加
  • 鉴权、限流、超时、上下文信息在哪里插入

这说明错误处理和中间件都不是附属细节,
而是整个请求处理链路的结构问题。

3. 先建立直觉

3.1 错误处理解决的是“失败如何被表达”

失败不是只有一种。
在 Web 服务里,你会碰到:

  • 参数不合法
  • 资源不存在
  • 权限不足
  • 数据库失败
  • 下游服务超时
  • 内部未知错误

真正的问题不是“会不会失败”,而是:

  • 这些失败对外应该长什么样
  • 哪些要暴露成 4xx
  • 哪些要暴露成 5xx
  • 哪些细节只该写日志,不该直接返回给客户端

3.2 中间件解决的是“重复横切逻辑放哪”

很多逻辑不是某个 handler 独有,而是很多接口都需要:

  • 请求日志
  • request id
  • tracing span
  • 鉴权
  • CORS
  • 超时
  • 压缩
  • 限流

如果这些都写进每个 handler,代码会很快失控。
所以中间件的意义就是:

  • 把这些横切能力从业务 handler 里抽出来,放到统一链路层处理

4. Rust Web 错误处理最核心的工程直觉

4.1 错误类型其实是在定义 HTTP 边界语义

在普通 Rust 模块里,错误类型定义模块边界。
在 Web 场景里,这件事会进一步表现成:

  • 某类错误最终映射成什么 HTTP 语义
  • 客户端能否稳定理解这个失败
  • 内部错误细节是否被妥善隐藏

所以 Web 错误处理不能只停留在“把 Err 传上去”,而要继续问:

  • 这个 Err 最后准备以什么响应形式出现

4.2 对外错误语义和内部诊断信息要分层

一个成熟服务里,错误至少有两层:

  • 对外层:返回给客户端的状态码、错误码、消息
  • 对内层:日志、source chain、上下文、堆栈、调用链信息

这两层不应该混在一起。

也就是说:

  • 客户端看到的错误应该稳定、清晰、可预期
  • 开发者看到的内部信息应该足够详细、可排查

4.3 统一错误出口很重要

如果每个 handler 都各自决定怎么拼错误响应,很快就会出现:

  • 格式不统一
  • 状态码风格混乱
  • 前端处理成本上升
  • 日志和错误映射重复

所以真正成熟的做法往往是:

  • 让不同类型的错误在边界层统一收敛
  • 用统一响应结构输出给客户端

这样项目的错误语义才会稳定。

5. Rust Web 中间件设计最核心的工程直觉

5.1 中间件不是“所有公共逻辑的垃圾桶”

虽然中间件承载横切逻辑,但不代表凡是公共代码都该塞进去。
更适合放进中间件的,通常是:

  • 与请求链路强相关
  • 对多个路由统一生效
  • 不属于某个具体业务领域

比如:

  • 日志
  • request id
  • tracing span
  • 鉴权前置检查
  • 限流
  • 通用 header 处理

而不是:

  • 某个具体业务流程的判断分支
  • 领域规则本身

5.2 中间件本质上是在控制“请求经过系统时被怎样包裹”

你可以把中间件想成一层层包裹请求处理链路的壳。
请求进入系统时:

  • 可能先经过日志记录
  • 再经过鉴权
  • 再经过 tracing 注入
  • 再进入 handler
  • 出错后再被统一映射成响应

所以中间件不是“附加组件”,而是请求生命周期的一部分。

5.3 顺序非常重要

中间件的执行顺序经常会直接影响行为:

  • 先打日志还是先鉴权
  • request id 在哪一层注入
  • timeout 应该包裹哪些逻辑
  • 错误映射发生在多靠外的一层

所以中间件设计不是“有没有”,而是“链路顺序是否合理”。

6. 错误处理和中间件为什么要一起设计

因为它们常常互相配合:

  • 中间件可以注入 request id,错误响应里可能需要带上它
  • tracing 中间件会记录错误上下文
  • 统一错误出口可能本身就依赖某层公共处理逻辑
  • 鉴权中间件可能提前拦截并产出标准化错误响应

也就是说,错误处理和中间件都在塑造同一件事:

  • 请求进入系统后,成功和失败分别如何被组织

7. Axum / Actix 里真正该优先建立的能力

不管你站在哪个框架上,先抓住这些能力最重要:

  1. 统一错误响应结构
  2. 清晰区分 4xx 和 5xx
  3. 内部诊断信息不裸露给外部
  4. 日志 / tracing 放进统一链路
  5. 鉴权、限流、request id 这类横切逻辑从 handler 中抽离
  6. 保持 handler 薄、边界清晰

只要这几件事做对,框架 API 细节差异反而没那么关键。

8. 常见误区

8.1 误区一:错误处理就是返回状态码

不够。
状态码只是外层结果,真正重要的是错误语义是否稳定、统一、可维护。

8.2 误区二:所有公共逻辑都应该做成中间件

不对。
只有真正的横切请求链路逻辑,才适合放进中间件。

8.3 误区三:只要能把错误打到日志里,外部响应随便点也没关系

不行。
客户端也需要稳定、可理解的失败语义。

8.4 误区四:中间件越多越专业

并不是。
层次过多、顺序混乱、职责不清,反而会让链路难以理解和调试。

9. 一个更实用的判断思路

当你在设计 Rust Web 的错误处理和中间件时,可以先问:

  1. 这个错误是给客户端看,还是给开发者排查看
  2. 这个逻辑是某个 handler 独有,还是全链路共享
  3. 这层逻辑应该放在 handler、服务层,还是中间件层
  4. 这个中间件的位置会不会影响日志、鉴权、超时或错误映射顺序
  5. 当前设计是否让 handler 更薄、更清晰,而不是更臃肿

10. 建议学习顺序

建议按这个顺序继续深入:

  1. 先建立统一错误响应结构
  2. 再梳理 4xx / 5xx 的分类方式
  3. 再加 request id、tracing、访问日志
  4. 再抽离鉴权、限流、超时等中间件
  5. 最后统一整理错误出口、中间件顺序和项目模块结构

11. 自测标准

  • 能解释 Web 错误处理为什么不只是“返回状态码”
  • 能知道对外错误语义与内部诊断信息应该分层
  • 能理解中间件的本质是承载横切请求链路逻辑
  • 能意识到中间件顺序会直接影响系统行为
  • 能判断某段逻辑该放在 handler、服务层还是中间件层